//------------------------------------------------------------------------------
// Copyright (c) 2008 www.dnaide.com
// Licensed under the MIT (MIT-LICENSE.txt)
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using System.Security.Cryptography;

namespace DNAide.Core
{
	public static partial class StringAide
	{

		/// <summary>
		/// Creates a strong password based the following defaults:
		///		- MinLength = 8
		///		- MaxLength = 8
		///		- LowercaseCharacters = abcdefgijkmnopqrstwxyz
		///		- UppercaseCharacters = ABCDEFGHJKLMNPQRSTWXYZ
		///		- NumericCharacters = 23456789
		/// </summary>
		/// <returns>String</returns>
		public static String GeneratePassword()
		{
			return GeneratePassword(8, 8, "abcdefgijkmnopqrstwxyz", "ABCDEFGHJKLMNPQRSTWXYZ", "23456789", "*$-+?_&=!%{}/");
		}


		/// <summary>
		/// Generates a password
		/// </summary>
		/// <param name="minLength">minLength</param>
		/// <param name="maxLength">maxLength</param>
		/// <param name="lowercaseCharacters">lowercaseCharacters</param>
		/// <param name="uppercaseCharacters">uppercaseCharacters</param>
		/// <param name="numericCharacters">numericCharacters</param>
		/// <param name="specialCharacters">specialCharacters</param>
		/// <returns>String</returns>
		public static String GeneratePassword(Int32 minLength, Int32 maxLength, String lowercaseCharacters, String uppercaseCharacters, String numericCharacters, String specialCharacters)
		{

			lowercaseCharacters = CastAide.AsString(lowercaseCharacters);
			uppercaseCharacters = CastAide.AsString(uppercaseCharacters);
			numericCharacters = CastAide.AsString(numericCharacters);
			specialCharacters = CastAide.AsString(specialCharacters);

			// Make sure that value parameters are valid.
			if ((minLength > maxLength) || (lowercaseCharacters.Length < 1 && uppercaseCharacters.Length < 1 && numericCharacters.Length < 1) && specialCharacters.Length < 1)
				return null;

			// Create a local array containing supported password characters
			// grouped by types. 
			char[][] charGroups = new char[4][];
			int groupCount = 0;
			if (lowercaseCharacters.Length > 0)
			{
				charGroups[groupCount] = lowercaseCharacters.ToCharArray();
				groupCount++;
			}
			if (uppercaseCharacters.Length > 0)
			{
				charGroups[groupCount] = uppercaseCharacters.ToCharArray();
				groupCount++;
			}
			if (numericCharacters.Length > 0)
			{
				charGroups[groupCount] = numericCharacters.ToCharArray();
				groupCount++;
			}
			if (specialCharacters.Length > 0)
			{
				charGroups[groupCount] = specialCharacters.ToCharArray();
				groupCount++;
			}
		

			// Use this array to track the number of unused characters in each
			// character group.
			int[] charsLeftInGroup = new int[groupCount];

			int index = 0;

			// Initially, all characters in each group are not used.
			for (index = 0; index < charsLeftInGroup.Length; index++)
				charsLeftInGroup[index] = charGroups[index].Length;

			// Use this array to track (iterate through) unused character groups.
			int[] leftGroupsOrder = new int[groupCount];

			// Initially, all character groups are not used.
			for (index = 0; index < leftGroupsOrder.Length; index++)
				leftGroupsOrder[index] = index;

			// This array will hold password characters.
			char[] password = null;

			// Allocate appropriate memory for the password.
			if (minLength < maxLength)
				password = new char[NumberAide.GenerateRandomInt32(minLength,maxLength + 1)];
			else
				password = new char[minLength];


			// Index of the next character to be added to password.
			int nextCharIdx;

			// Index of the next character group to be processed.
			int nextGroupIdx;

			// Index which will be used to track not processed character groups.
			int nextLeftGroupsOrderIdx;

			// Index of the last non-processed character in a group.
			int lastCharIdx;

			// Index of the last non-processed group.
			int lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;

			// Generate password characters one at a time.
			for (index = 0; index < password.Length; index++)
			{
				// If only one character group remained unprocessed, process it;
				// otherwise, pick a random character group from the unprocessed
				// group list. To allow a special character to appear in the
				// first position, increment the second parameter of the Next
				// function call by one, i.e. lastLeftGroupsOrderIdx + 1.
				if (lastLeftGroupsOrderIdx == 0)
					nextLeftGroupsOrderIdx = 0;
				else
					nextLeftGroupsOrderIdx = NumberAide.GenerateRandomInt32(0, lastLeftGroupsOrderIdx);

				// Get the actual index of the character group, from which we will
				// pick the next character.
				nextGroupIdx = leftGroupsOrder[nextLeftGroupsOrderIdx];

				// Get the index of the last unprocessed characters in this group.
				lastCharIdx = charsLeftInGroup[nextGroupIdx] - 1;

				// If only one unprocessed character is left, pick it; otherwise,
				// get a random character from the unused character list.
				if (lastCharIdx == 0)
					nextCharIdx = 0;
				else
					nextCharIdx = NumberAide.GenerateRandomInt32(0, lastCharIdx + 1);

				// Add this character to the password.
				password[index] = charGroups[nextGroupIdx][nextCharIdx];

				// If we processed the last character in this group, start over.
				if (lastCharIdx == 0)
					charsLeftInGroup[nextGroupIdx] =
						charGroups[nextGroupIdx].Length;
				// There are more unprocessed characters left.
				else
				{
					// Swap processed character with the last unprocessed character
					// so that we don't pick it until we process all characters in
					// this group.
					if (lastCharIdx != nextCharIdx)
					{
						char temp = charGroups[nextGroupIdx][lastCharIdx];
						charGroups[nextGroupIdx][lastCharIdx] =
							charGroups[nextGroupIdx][nextCharIdx];
						charGroups[nextGroupIdx][nextCharIdx] = temp;
					}
					// Decrement the number of unprocessed characters in
					// this group.
					charsLeftInGroup[nextGroupIdx]--;
				}

				// If we processed the last group, start all over.
				if (lastLeftGroupsOrderIdx == 0)
					lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;
				// There are more unprocessed groups left.
				else
				{
					// Swap processed group with the last unprocessed group
					// so that we don't pick it until we process all groups.
					if (lastLeftGroupsOrderIdx != nextLeftGroupsOrderIdx)
					{
						int temp = leftGroupsOrder[lastLeftGroupsOrderIdx];
						leftGroupsOrder[lastLeftGroupsOrderIdx] =
							leftGroupsOrder[nextLeftGroupsOrderIdx];
						leftGroupsOrder[nextLeftGroupsOrderIdx] = temp;
					}
					// Decrement the number of unprocessed groups.
					lastLeftGroupsOrderIdx--;
				}
			}

			return new string(password);
		}

	}
}
